<?php
namespace app\common\traits;

use think\model\Collection;

/**
 * 支持(无限层级)父子节点的 trait.
 * 一般用于自身可以形成父子层级的一个模型, 比如 AuthRule/Category.
 */
trait NodeTrait
{
    /**
     * @param self[] 子节点.
     */
    protected $_children = [];
    /**
     * @param self|null 父节点.
     */
    protected $_parent = null;
    /**
     * 父id的字段名称.
     */
    protected $_pidField = 'pid';

    /**
     * 添加子节点, 注意这个函数不检查子节点是否重复, 也不持久化数据.
     * 一般用来构建现有数据的父子结构关系.
     * @param self $child 子节点.
     */
    public function addChildren($child)
    {
        $this->_children[] = $child;
        $child->setParentObject($this);
    }

    /**
     * 获取父节点id.
     * @return string|int
     */
    public function getParentId()
    {
        return $this->{$this->_pidField};
    }

    /**
     * 设置父节点id.
     * @param int $pid
     */
    public function setParentId($pid)
    {
        $this->{$this->_pidField} = $pid;
    }

    /**
     * 获取祖先层级.
     * @return self[]
     */
    public function getAncestorHierarchy()
    {
        $hierarchy = [$this];
        $cur = $this;
        while ($cur->_parent) {
            array_unshift($hierarchy, $cur->_parent);
            $cur = $cur->_parent;
        }
        return $hierarchy;
    }

    /**
     * 设置父节点.
     * @param self $parent
     */
    public function setParentObject($parent)
    {
        $this->setParentId($parent->getParentId());
        $this->_parent = $parent;
    }

    /**
     * 获取父节点.
     * @return self|null
     */
    public function getParent()
    {
        return $this->_parent;
    }

    /**
     * 是否有子节点.
     * @return boolean
     */
    public function hasChildren()
    {
        return count($this->_children);
    }

    /**
     * 获取所有子节点.
     * @return self[]
     */
    public function getChildren()
    {
        return $this->_children;
    }

    /**
     * 根据一维节点列表(即模型数组)构建一个有正确父子关系的树状集合并返回该集合.
     * 注意, 数据结构不完整的(比如父节点不存在的子节点)部分会被直接忽略掉.
     * @param array $list 一维节点列表(已经排好序).
     * @return Collection
     */
    public static function buildTree($list)
    {
        // 根节点对象数组.
        $roots = [];
        // [id => obj, ...] 的数组
        $objMaps = [];
        foreach ($list as $obj) {
            $objMaps[$obj->id] = $obj;
        }

        foreach ($list as $obj) {
            $pid = $obj->getParentId();
            // $pid 为 0 说明是没有父级的节点, 也就是根节点.
            if ((int)$pid === 0) {
                $roots[$obj->id] = $obj;
            } else {
                if (isset($objMaps[$pid])) {
                    $objMaps[$pid]->addChildren($obj);
                }
            }
        }
        return new Collection($roots);
    }

    /**
     * 将当前节点对象的数据转换为 fancyTree 节点使用的数据.
     *
     * fancytree 中:
     * - key 是主键, title 是标题, children 是子级(数组)
     * - icon (字符串), 表示显示的图标 (比如 'fa fa-home')
     * - folder (布尔类型), 表示是否为文件夹, checkbox (布尔类型), 表示是否显示勾选项
     * - expanded (布尔类型), 表示是否为展开状态
     * 更详细说明见:
     * @link https://wwwendt.de/tech/fancytree/doc/jsdoc/global.html#FancytreeOptions
     *
     * @return array
     */
    public function getFancyTreeNode()
    {
        return [
            'key' => $this->id,
            'title' => $this->getData('name'),
        ];
    }

    /**
     * 将对象集合转换为 fancytree 使用的数据数组形式.
     * @param self[]|Collection $roots 有完整父子关系的根节点对象集合/数组.
     * @return array[]
     */
    public static function toFancyTreeData($roots)
    {
        $array = [];
        foreach ($roots as $node) {
            $n = $node->getFancyTreeNode();
            if ($node->hasChildren()) {
                $n['children'] = self::toFancyTreeData($node->getChildren());
            }
            $array[] = $n;
        }

        return $array;
    }
}
